using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using TelnetExpect;
using System.Threading;

namespace TelnetExpect
{
    /// <summary>
    /// Provides UNIX Expect-like scripting of terminal connections.
    /// </summary>
    /// -
    /// <example>
    /// <code>
    /// TcpClient cli = new TcpClient(hostName, TelnetStream.DefaultPort);
    /// TelnetStream telnet = new TelnetStream(cli.GetStream());
    /// // Don't want to see our keystrokes echoed back!
    /// telnet.SetRemoteMode(TelnetOption.Echo, false);
    /// m_xp = new Expector(telnet);
    /// //
    /// m_xp.Expect("login:");
    /// m_xp.SendLine(m_username);
    /// m_xp.Expect("password:");
    /// m_xp.SendLine(m_password);
    /// m_xp.Expect(">");
    /// m_xp.SendLine("net start");
    /// m_xp.SendLine("exit");
    /// // If the service is running, it will be listed by "net start" and
    /// // the following line will match it.  If not we'll read to EoF and
    /// // an error will be produced.  (EndOfStreamException if the exit
    /// // command works, or IOException if timeout occurs).
    /// m_xp.Expect("   " + serviceDescr);
    /// // If we got here then the service is running!
    /// </code>
    /// </example>
    public class Expector : IDisposable
    {
        TextReader m_rdr;
        TextWriter m_wtr;
        const bool m_ownsStream = true;
        //
        DateTime? m_lastStep;
        static readonly TimeSpan DefaultExpectTimeout = new TimeSpan(0, 2, 0);
        TimeSpan m_expectTimeout = DefaultExpectTimeout;
        //
        char[] m_buf = new char[1024];
        int m_totalLen;

        //----
        public Expector(Stream conn)
        {
            Encoding enc = Encoding.ASCII;
            m_rdr = new StreamReader(conn, enc);
            m_wtr = new StreamWriter(conn, enc);
        }

        //----
        public void SendLine(string txt)
        {
            m_wtr.WriteLine(txt);
            m_wtr.Flush();
        }

        //----
        public TimeSpan ExpectTimeout
        {
            get { return m_expectTimeout; }
            set { m_expectTimeout = value; }
        }

        //----
        public void Expect(string xp)
        {
            Trace.WriteLine("----------------");
            Trace.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
                "Expect [[{0}]]",
                TelnetBaseStream.UtilityShowControlChars(xp)));
            m_lastStep = DateTime.UtcNow;
            ManualResetEvent completed = null;
            try {
                RegisterTimeoutIfEnabled(ref completed);
                Expect2(xp);
            } finally {
                if (completed != null) {
                    completed.Set();
                }
            }
        }

        private void RegisterTimeoutIfEnabled(ref ManualResetEvent completed)
        {
            if (m_expectTimeout != TimeSpan.MinValue
                    && m_expectTimeout != TimeSpan.Zero) {
                completed = new ManualResetEvent(false);
                ThreadPool.RegisterWaitForSingleObject(completed,
                    ExpectTimeouter, null,
                    m_expectTimeout, true);
            }
        }

        void Expect2(string xp)
        {
            // Check in the buffer first in case the next match is already there.
            while (true) {
                string line = new string(m_buf, 0, m_totalLen);
                int matchIdx = line.IndexOf(xp, StringComparison.InvariantCulture);
                Trace.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
                    "Expect {0} find match in [[{4}]]",
                    (-1 != matchIdx) ? "did" : "didn't",
                    xp, TelnetBaseStream.UtilityShowControlChars(xp),
                    line, TelnetBaseStream.UtilityShowControlChars(line)
                    ));
                if (-1 != matchIdx) {
                    // Discard all of the _used_ bit of the text!
                    int keepFrom = matchIdx + xp.Length;
                    int keepCount = m_totalLen - keepFrom;
                    Debug.Assert(keepCount >= 0, "We matched, so there must be at least that number of chars in the buffer.");
                    Array.Copy(m_buf, keepFrom, m_buf, 0, keepCount);
                    m_totalLen -= keepFrom;
                    Debug.Assert(m_totalLen == keepCount, "maths");
                } else {
                    // In case part of the match is in this bit and part is in the
                    // next, keep part of the content for appending to.  We can
                    // discard most, keeping only a piece one char smaller than the 
                    // pattern (we don't need to same length, as there would have
                    // been a match if the whole length matched...)
                    // (If the pattern was a regexp we'd probably have to keep the
                    // whole lot).
                    int keepCount = xp.Length - 1;
                    keepCount = Math.Min(m_totalLen, keepCount);
                    Array.Copy(m_buf, m_totalLen - keepCount, m_buf, 0, keepCount);
                    m_totalLen = keepCount;
                }
                string tmp = new string(m_buf, 0, m_totalLen);
                Trace.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
                    "Keeping: [[{4}]]",
                    (-1 != matchIdx) ? "did" : "didn't",
                    xp, TelnetBaseStream.UtilityShowControlChars(xp),
                    tmp, TelnetBaseStream.UtilityShowControlChars(tmp)));
                if (-1 != matchIdx) {
                    return;
                }
                // Need to get more to match against.
                int space = m_buf.Length - m_totalLen;
                if (space <= 0) {
                    // This will only happen if the input match string is bigger 
                    // that the buffer.  We truncate the buffer content after each
                    // read; in particular, after a non match we discard all but
                    // pattern.Length elements.
                    throw new InvalidOperationException("We're gonna need a bigger buffer!!!!!!!");
                }
                // Can't use m_rdr.ReadLine as e.g. "login: " doesn't end with a newline!
                int readLen = m_rdr.Read(m_buf, m_totalLen, space);
                if (readLen == 0) {
                    throw new EndOfStreamException("No match before end of input.");
                }
                m_totalLen += readLen;
            }//while
        }

        //----
        void ExpectTimeouter(object state, bool timedOut)
        {
            if (timedOut) {
                Trace.WriteLine("Expect is closing connection due to timeout.");
                try {
                    m_wtr.Close();
                } finally {
                    m_rdr.Close();
                }
            }
        }


        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing && m_ownsStream) {
                try {
                    // Probably no need to close each side, but just in case...
                    m_wtr.Close();
                } finally {
                    m_rdr.Close();
                }
            }
        }

        #endregion
    }
}
